iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0
Modern Web

Angular 全集中筆記系列 第 19

第 19 型 - HttpClient

  • 分享至 

  • xImage
  •  

上一篇將待辦事項資料從元件 (Component) 抽離至服務 (Service) 中;接下來,這一篇將利用 Angular 內建的 httpClient 從伺服器取得待辦事項資料。

前置作業

為了可以從伺服器中取得資料,因此需要在終端機執行 npm install -g json-server,透過 json-server 來模擬後端服務。並在 task.ts 加入 id 屬性,以便之後可以新增、編輯與刪除資料。

json-server

json-server 主要用來模擬後端服務的 API,並不建議使用在正式環境中。

import { TaskState } from "../enum/task-state.enum";

export class Task {
  constructor(
    public id: number,
    public subject: string,
    public state: TaskState = TaskState.None
  ) {}

  level?: "XS" | "S" | "M" | "L" | "XL";

  tags?: string[];

  expectDate?: Date;

  finishedDate?: Date;
}

接著,在 /src/assets/data 加入 db.json 檔案,並在此檔案加入待辦事項資料清單,此檔案即為 json-server 所存取的資料。

{
  "tasks": [
    {
      "id": 1,
      "subject": "頁面需要顯示待辦事項主旨",
      "state": 0,
      "level": "XS",
      "tags": ["FEATURE", "ISSUE", "enhancement", "discussion"]
    },
    {
      "id": 2,
      "subject": "可以設定待辦事項的狀態",
      "state": 1,
      "level": "S",
      "tags": ["Feature", "Issue", "document"],
      "expectDate": "2020-10-31T16:00:00.000Z"
    },
    {
      "id": 3,
      "subject": "頁面需要顯示待辦事項主旨",
      "state": 2,
      "level": "M",
      "tags": ["feature", "issue"],
      "expectDate": "2020-09-30T16:00:00.000Z",
      "finishedDate": "2020-09-30T16:00:00.000Z"
    }
  ]
}

json-server

最後,在終端機執行 json-server src/assets/data/db.json,就可以透過 GET、POST、PUT 與 DELETE 等 HTTP 方法針對 http://localhost:3000/tasks 路徑來新增、修改與刪除待辦事項資料。

json-server

利用 Angular 內建的 HttpClient 取得後端資料

Angualr 內建的 HttpClient 類別提供了 get()post()put()delete() 等方法對應 HTTP 方法來使用後端服務 API。因此,先執行 'ng g s task/services/task-remote' 指令來建立 TaskRemoteService 服務,並在此服務中利用 HttpClient 的 get() 方法取得資料;不過在使用之前需要將 HttpClientModule 模組匯入 TaskModule 內。

@NgModule({
  imports: [CommonModule, HttpClientModule],
  declarations: [
    TaskComponent,
    TaskStateColorDirective,
    TaskListComponent,
    TaiwanDatePipe,
  ],
  exports: [TaskComponent, TaskListComponent],
})
export class TaskModule {}

在 Httpclient 類別內的方法,會以非同步發出一個 HTTP 請求,並回傳的是 Observable 型別;利用 Observable 的回傳值,可以監控與訂閱非同步請求的狀態,或利用 RxJs 來調整資料內容的狀態。因此,在 task-remote.service.ts 中的 getData() 會使用 HttpClient.get() 方法來取得待辦事項,並回傳一 Observable<Task[]> 型別資料。

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";

import { Task } from "../../model/task";

@Injectable({
  providedIn: "root",
})
export class TaskRemoteService {
  private _url = "http://localhost:3000/tasks";

  constructor(private httpClient: HttpClient) {}

  getData(): Observable<Task[]> {
    return this.httpClient.get<Task[]>(this._url);
  }
}

最後,在 task-list.component.ts 注入 TaskRemoteService 服務,並訂閱 getDate() 方法將取得到的清單資料設定至待辦事項屬性中。

import { Component, OnInit } from "@angular/core";

import { Task } from "../../model/task";
import { TaskRemoteService } from "../services/task-remote.service";

@Component({
  selector: "app-task-list",
  templateUrl: "./task-list.component.html",
  styleUrls: ["./task-list.component.css"],
})
export class TaskListComponent implements OnInit {
  tasks: Task[];

  constructor(private taskService: TaskRemoteService) {}

  ngOnInit(): void {
    this.taskService.getData().subscribe((tasks) => (this.tasks = tasks));
  }
}

需要注意的是 Observable 資料型別需要訂閱 (subscribe) 才會被執行。

Result

利用 AsyncPipe 訂閱 Observable 物件

從上面結果可看到是否為沒有待辦事項的顯示判斷中拋出了例外,這是因為使用了非同步取得資料,而在資料未取得到之前,tasks 屬性值則為 undefined 而拋出例外。要解決這個問題,可以在 task-list.component.html 利用 ? 關鍵字,使其在 tasks 屬性為 undefined 時不取得 'length' 資訊。

<ng-container
  *ngIf="tasks?.length === 0; then dataEmpty; else list"
></ng-container>

另外,Angular 也提供了 AsyncPipe 來訂閱 Observable 物件。因此,也可以將 task-list.component.tstasks 屬性修改為 tasks$: Observable<Task[]>,以表示一個可監控的待辦事項屬性;接著將此屬性值設定待辦事項服務的 getData() 方法回傳值。

import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";

import { Task } from "../../model/task";
import { TaskRemoteService } from "../services/task-remote.service";

@Component({
  selector: "app-task-list",
  templateUrl: "./task-list.component.html",
  styleUrls: ["./task-list.component.css"],
})
export class TaskListComponent implements OnInit {
  tasks$: Observable<Task[]>;

  constructor(private taskService: TaskRemoteService) {}

  ngOnInit(): void {
    this.tasks$ = this.taskService.getData();
  }
}

在 Angular 的命名習慣上,會用 $ 作為結尾來代表 Observable 物件。

而在 task-liet.component_html 中,利用 AsyncPipe 訂閱 task$ 屬性,在取得到資料時顯示 #list 範本,否則顯示 #dataEmpty

<ng-container *ngIf="tasks$ | async; then list; else dataEmpty"></ng-container>

<ng-template #dataEmpty>
  <div class="data-empty">無待辦事項</div>
</ng-template>

<ng-template #list>
  <app-task
    *ngFor="let task of tasks$ | async; let odd = odd"
    [class.odd]="odd"
    [subject]="task.subject"
    [(state)]="task.state"
    [level]="task.level"
    [tags]="task.tags"
    [expectDate]="task.expectDate"
    [finishedDate]="task.finishedDate"
  ></app-task>
</ng-template>

AsyncPipe

不過,從上面結果可以看到,因為使用了兩次 AsyncPipe 而發送了兩次 Http 請求,進一步影響了使用者體驗。此時可以透過在 AsyncPipe 後加入 as 關鍵字 ,來建立樣版區域變數供 ngFor 使用。

<ng-container *ngIf="tasks$ | async as tasks; else dataEmpty">
  <app-task
    *ngFor="let task of tasks; let odd = odd"
    [class.odd]="odd"
    [subject]="task.subject"
    [(state)]="task.state"
    [level]="task.level"
    [tags]="task.tags"
    [expectDate]="task.expectDate"
    [finishedDate]="task.finishedDate"
  ></app-task>
</ng-container>

<ng-template #dataEmpty>
  <div class="data-empty">無待辦事項</div>
</ng-template>

AsyncPipe

結論

這一篇利用了 HttpClient 類別,來從後端服務 API 取得資料清單,並進一步使用了 AsyncPipe 訂閱 Observable 物件,實作程式碼放在 GitHub


上一篇
第 18 型 - 服務 (Service)
下一篇
第 20 型 - 依賴注入 (Dependency Injection, DI)
系列文
Angular 全集中筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
ShawnGood
iT邦新手 5 級 ‧ 2022-03-15 12:37:49

謝謝你的分享,但是關於這個code

<ng-container
  *ngIf="tasks?.length === 0; then dataEmpty; else list"
></ng-container>

「當tasks為undefined就不會執行.length」
相當於

// 偷推一下JSX
if(undefined===0) 
    dataEmpty
else 
    list

(undefined===0)的結果是false
分支會走向list
這也合理,還沒要到資料,並不代表「無待辦事項」

但下面這部分

ng-container *ngIf="tasks$ | async as tasks; else dataEmpty">
  <app-task
    *ngFor="let task of tasks; let odd = odd"
    [class.odd]="odd"
    [subject]="task.subject"
    [(state)]="task.state"
    [level]="task.level"
    [tags]="task.tags"
    [expectDate]="task.expectDate"
    [finishedDate]="task.finishedDate"
  ></app-task>
</ng-container>

<ng-template #dataEmpty>
  <div class="data-empty">無待辦事項</div>
</ng-template>

在tasks.length為0時,不會進到dataEmpty也
這裡的else 是不是指subscribe失敗了?
自答一下:原來還沒subscribe到資料前,會進else
我懂了所以上面其實是

<ng-container *ngIf="tasks$ | async as tasks; else tasksUndefined">
  <app-task
    *ngFor="let task of tasks; let odd = odd"
    [class.odd]="odd"
    [subject]="task.subject"
    [(state)]="task.state"
    [level]="task.level"
    [tags]="task.tags"
    [expectDate]="task.expectDate"
    [finishedDate]="task.finishedDate"
  ></app-task>
</ng-container>

<ng-template #tasksUndefined>
  <div class="data-loading">還在loading</div>
</ng-template>

我要留言

立即登入留言